Skip to content

Skills registry: pull, bind and qualify attested skills - #1623

Merged
abrichr merged 2 commits into
mainfrom
feat/skills-registry-v2
Oct 29, 2026
Merged

abrichr merged 2 commits into
mainfrom
feat/skills-registry-v2

Conversation

@abrichr

@abrichr abrichr commented Oct 29, 2026

Copy link
Copy Markdown
Member

Replaces #1592 with one squashed commit, so the secret scan, which reads the PR's history, no longer sees two test values from its earlier commits. The content is #1592's final tree (e8a161b).

Review history:

Since that reply: sigstore (the registry and dev extras) is limited to Python 3.11+, because on 3.10 it resolves dnspython 2.8.0 (GHSA-45c3-73f7-pv4m). Two test values that looked like credentials were replaced with plain text.


Stage 1 of the verified-skills registry. The design is in packages/flow/docs/design/SKILLS_REGISTRY.md, and it builds on openadapt pull (#1173).

A skill pulled from the registry runs only after it is qualified on the user's own instance: a registry certification is evidence about our test copy, never about theirs. Who published the index is established keylessly, with GitHub artifact attestations, as for our other releases.

Who published it: keyless attestation

  • The release. .github/workflows/release-registry.yml runs in stages:

    • A manual dispatch from main tags registry-<suffix> with the release App.
    • On the tag push, an unprivileged check job runs the bundle checks.
    • The attest job then runs in release-identity, and it is the only job with id-token: write. It builds reproducible archives, stamps the index (issued_at, a 30-day expires_at, and serial) and attests index.json and every archive with actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6. It checks its own attestation with the pull verifier, using only hash-pinned wheels (registry/verify-requirements.txt).
    • publish releases the archives, index.json and index.sigstore.json. It opens a PR with the index and its attestation, adding both by name.
  • The check. pull verifies index.sigstore.json with the sigstore package (the new registry extra, so the base install doesn't grow). The check is offline against openadapt_flow/registry_trusted_root.json, for a pinned identity:

    • issuer https://token.actions.githubusercontent.com;
    • SAN https://github.com/OpenAdaptAI/OpenAdapt/.github/workflows/release-registry.yml@refs/tags/registry-*;
    • source repository https://github.com/OpenAdaptAI/OpenAdapt.

    The statement must name the sha256 of the exact index that was read, and of any archive pulled.

  • Refreshing trust. pull --refresh-trust fetches the current Sigstore trusted root through Sigstore's TUF repository into $OPENADAPT_HOME. After that, verification uses the refreshed root.

  • Rollback and freeze. The attested index carries issued_at, expires_at and serial. pull refuses an expired index, one issued in the future, and a serial lower than the highest it has seen from that address ($OPENADAPT_HOME/registry-state.json). An entry dropped from the index changes its digest.

  • Local only. For an index being authored on the local disk:

    • --allow-unsigned takes one with no attestation;
    • --allow-test-key takes one attested by the local test signer (registry_attest.sign_test).

    Both flags are refused for a remote index, and plain http is refused.

  • Removed:

    • the signing seed (REGISTRY_SIGNING_SEED) and the registry-signing environment;
    • the Ed25519 entry and index signatures;
    • registry_keys.json;
    • the custom registry_index_sha256 Rekor anchor (the attestation is already in Rekor).

Pull, bind, qualify, run

  • Provenance in the workflow. pull stores Workflow.registry: the entry id, version, index, the entry's sha256, who attested it and the published digest. The content digest covers it. pull also records the program's steps outside the bundle ($OPENADAPT_HOME/pulled.json), so a bundle with its provenance stripped is still refused. It writes skill.lock.json and expires any certification the bundle carries.
  • skill bind records the user's values for the contract's declared variables: URLs, environment variable names, local paths and local commands. Each is checked against its kind, and a credential is only ever a variable name. The binding file is written mode 600.
  • skill qualify runs the qualification cases in place against the bound instance. It uses the private tier's full check when OPENADAPT_REGISTRY_TOKEN is set, else the bundled minimal one.
    • A shipped contract never runs code it names and never reads the environment. Before expansion, its reset command and URL, its addresses, credential names and paths must each be a declared ${VAR}. Hooks and plugins are refused, and expansion uses the binding alone.
    • The user's own --verify counts only for a skill that ships none, or with --replace-contract.
  • The refusal. run, replay, emit-skill and emit-mcp refuse a pulled skill without a current certification on a named instance, recorded by skill qualify for this digest. run and replay check the deployment's app address. An emitted web MCP server checks every call's URL against the qualified instance.
  • The private tier. full_contract takes exactly a url and a sha256. The token is sent only over https to a host in registry.PRIVATE_TIER_HOSTS, checked before it is sent. The body is size-capped and kept beside the bundle, never in it.
  • Archives are https-only, or inside a local index's directory. Member sizes are checked before any read, the total and the count are capped, and every member must be in the entry or be the license NOTICE.
  • Public bundles hold only allowlisted files. Screen templates need the entry's screens.data: synthetic attestation. Versions order as semver.

Not in this PR

Tests

  • tests/test_skills_registry.py and tests/test_registry.py: the index, pull, bind, qualify and the refusal; the attestation (pull and record the attester; no attestation; the test signer needs the flag; a changed index, signature or subject); and archives.

  • tests/test_skills_registry_review.py: one or more tests per supply-chain review item, plus the Sigstore path.

  • The real Sigstore path. The fixture is an attestation GitHub made for the OpenAdapt Desktop 0.24.1 SHA256SUMS release asset (desktop-release.yml). It is verified offline with the pinned trusted root and must name that file's sha256. These are refused:

    • the same bundle checked against the pinned registry identity (another workflow);
    • a changed statement;
    • a trusted root without Fulcio's keys.

    These tests skip when sigstore isn't installed, and run in CI, because dev includes it.

Run locally at 4c584fd: 142 passed with sigstore installed. Without it, 50 passed and the 4 real-path tests skipped. The workflow-policy, packaging and capabilities tests pass, and ruff, ruff format, mypy and actionlint are clean.

🤖 Generated with Claude Code

A registry of compiled skills. pull verifies a keyless Sigstore attestation
from the registry release workflow, naming the exact index and archives;
refuses an expired index and one older than the newest seen from the same
attesting identity at any address; and marks the pulled bundle so run,
replay and emit refuse it until skill qualify passes on the user's own
instance.
file=sys.stderr,
)
return 1
print(f"Sigstore trusted root refreshed: {path}")
… 2.x in the test

The check job installed the engine without hashes; it now uses
locked_install.py like every other job. The shipped Sigstore trusted root
is added to the reviewed public artifact inventory. The emitted MCP server
test stands in for FastMCP, which mcp 2.x renamed, as test_emit does.
@abrichr
abrichr merged commit e73d684 into main Oct 29, 2026
41 of 42 checks passed
@abrichr
abrichr deleted the feat/skills-registry-v2 branch October 29, 2026 01:50
abrichr added a commit that referenced this pull request Oct 29, 2026
#1582, #1588, #1599, #1600, #1604, #1622, #1623 and #1624 merged after the
2.4.1 bump and before 2.4.1 published, so they ship in it. Adds their lines,
updates the summary and regenerates llms-full.txt.
abrichr added a commit that referenced this pull request Oct 29, 2026
…guards back on the PR lane (#1632)

Main's Packages run fails tests/test_cli_hints.py::test_printed_hints_never_hard_code_the_engine_spelling
on registry.py's pull hint (#1623), which printed a literal "openadapt-flow
skill ...". Under the launcher that names a program the person never typed;
it now goes through cli_hints.command() like every other hint.

#1623's own CI never ran the test: #1608 moved it to nightly as a 6 s test,
though it is a source scan that guards every printed hint. It returns to the
pull-request lane, with five safety guards the same rule moved by mistake:
identity (a numeric MRN homonym never verifies), durable resume (never
resends), the safety gallery (every dangerous case is safe), halt detail (a
typed value never appears) and self-repair (a healthy run never calls the
agent). Together they add about 90 test-seconds across the four shards.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants